﻿#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//sizeof和strlen的对比
//sizeof
    //在之前学习操作符的时候，我们学习了 sizeof ， sizeof 计算变量所占内存空间大小的，单位是字节，
    //如果操作数是类型的话，计算的是使用类型创建的变量所占内存空间的大小。
    //sizeof是单目操作符，不是函数！
//int main()
//{
//    int a = 10;
//    printf("%d\n", sizeof(a));
//    printf("%d\n", sizeof a); //这里把a的括号去掉也能计算变量a类型的大小，证明了sizeof并不是函数，函数后面的括号不能省略
//    printf("%d\n", sizeof(int));
//	return 0;
//}

//strlen
    //strlen 是C语言库函数，功能是求字符串⻓度。
    //函数原型如下：size_t strlen(const char* str);
    //统计的是从 strlen 函数的参数 str 中这个地址开始向后， \0 之前字符串中字符的个数。
    //strlen 函数会一直向后找 \0 字符，直到找到为止，所以可能存在越界查找。
//int main()
//{
//    char* str = "abc";
//    printf("%p\n", str);
//    printf("%p\n", "abc"); //通过调试我们可以知道：字符串变量存储的就是首字符的内存地址
//    return 0;
//}

//int main()
//{
//    size_t len = strlen("abcdef"); //由于字符串变量存储的就是首字符的内存地址，
//    printf("%zd\n", len);
//    const char* str = "abcdef";    //又因为字符指针str接收的也是字符串首字符的地址
//    printf("%zd\n", strlen(str)); 
//    char arr[] = "abcdef";
//    printf("%zd\n", strlen(arr));  //又因为数组名就是首元素的地址，所以字符数组arr接收的也是字符串首字符的地址
//    return 0;
//} 
//    //这样strlen就有三种写法，一种是直接传入字符串，一种是传入字符指针，另一种就是传入字符数组名

//sizeof 和 strlen的对比
    //sizeof                                           strlen
    //1. sizeof是操作符                                1. strlen是库函数，使用需要包含头文件 string.h 
    //2. sizeof计算操作数所占内存的大小，单位是字节    2. srtlen是求字符串长度的，统计的是 \0 之前字符的个数
    //3. 不关注内存中存放什么数据                      3. 关注内存中是否有 \0 ，如果没有 \0 ，就会持续往后找，可能会越界
    //4. sizeof括号内如果有表达式的话，表达式不参与计算

//int main()
//{
//    int a = 10;
//    short s = 4;
//    printf("%d\n", sizeof(s = a + 1)); //2
//    printf("%d\n", s); //4  s = a + 1并没有参与计算，s的值没有发生改变
//    return 0;
//}

//数组笔试题解析
//⼀维数组
    //回顾数组名的理解
    //数组名是数组首元素的地址
    //但是有2个例外：
    //1. sizeof(数组名) -- 数组名表示整个数组，计算的是整个数组的大小，单位是字节
    //2. &数组名 -- 数组名表示整个数组，取出来的是整个数组的地址
    //只要不是这两个例外，数组名就等价于数组首元素的地址
 
//int main()
//{
//    int a[] = { 1,2,3,4 };
//    printf("%d\n", sizeof(a)); //数组名的例外：sizeof(数组名) -- 计算整个数组的大小 16
//    printf("%d\n", sizeof(a + 0));//首先括号内的已经不是单独的数组名也就不是例外，a就是首元素的地址，类型为int*
//                                  //a + 0跳过0个字节还是首元素地址，是地址sizeof就是计算指针类型的大小 4/8
//    printf("%d\n", sizeof(*a)); //和上面一样不是例外，a就是首元素的地址，对其解引用则*a就是首元素，类型为int 4
//    printf("%d\n", sizeof(a + 1));//和a + 0一样还是地址，是第2个元素的地址，但只要是地址sizeof就是计算指针类型的大小 4/8
//    printf("%d\n", sizeof(a[1]));//a[1]就是第2个元素，类型为int 4
//    printf("%d\n", sizeof(&a));//这个代码相对会纠结，首先&a是数组名的例外，此时数组名表示整个数组，可能就有人会以为是16
//                               //但是再想想，sizeof括号里面是不是只有数组名，并不是，而是&数组名，类型是int(*)[4]
//                               //&数组名取出来的是整个数组的地址，归根结底还是地址，是地址sizeof就是计算指针类型的大小 4/8
//    printf("%d\n", sizeof(*&a));//之前我们就知道 *& 是相互抵消的，所以*&a == a，所以sizeof(*&a) == sizeof(a) 16
//    printf("%d\n", sizeof(&a + 1));//&a + 1是跳过整个数组后的位置的地址，是地址sizeof就是计算指针类型的大小 4/8
//    printf("%d\n", sizeof(&a[0]));//&a[0]就是首元素的地址，是地址sizeof就是计算指针类型的大小 4/8
//    printf("%d\n", sizeof(&a[0] + 1));//&a[0] + 1 == &a[1]就是第2个元素的地址，是地址sizeof就是计算指针类型的大小 4/8
//    return 0;
//}

//字符数组
//代码1：
//int main()
//{
//    char arr[] = { 'a','b','c','d','e','f' };
//    printf("%d\n", sizeof(arr));//数组名单独放在sizeof里面，属于数组名的例外，计算的是整个数组的大小 6
//    printf("%d\n", sizeof(arr + 0));//和上面一样，括号里面不是单独数组名，所以a + 0还是首元素地址，是地址就是4/8
//    printf("%d\n", sizeof(*arr));//arr是数组首元素地址，解引用得到的就是首元素，类型是char，大小为1
//    printf("%d\n", sizeof(arr[1]));//arr[1]就是第2个元素，类型是char，大小为1
//    printf("%d\n", sizeof(&arr));//&arr是数组指针，类型是char(*)[6],归根结底还是地址，是地址就是4/8
//    printf("%d\n", sizeof(&arr + 1));//&arr + 1是跳过整个数组后的位置的地址，是地址就是4/8
//    printf("%d\n", sizeof(&arr[0] + 1));//&arr[0] + 1 == &arr[1],就是第2个元素的地址，是地址就是4/8
//    return 0;
//}

#include <string.h>
//代码2：
//int main()
//{
//    char arr[] = { 'a','b','c','d','e','f' };
//    printf("%d\n", strlen(arr));//不是两个例外，arr数组名就是数组首元素的地址，数组中没有 \0 ，就会导致越界访问，结果是随机的
//    printf("%d\n", strlen(arr + 0));//arr + 0还是数组首元素的地址，数组中没有 \0 ，就会导致越界访问，结果是随机的
//  //printf("%d\n", strlen(*arr));//arr数组名是数组首元素的地址，对其解引用得到的是'a'这个元素，并不是地址
//                                   //'a'的ascii值是97，相当于把97作为地址传给strlen，strlen得到的就是野指针，代码是有问题的
//  //printf("%d\n", strlen(arr[1]));//arr[1]就是第2个元素'b'，也不是地址，和上面一样，传给strlen函数是错误的
//    printf("%d\n", strlen(&arr));//&arr是整个数组的地址，但值和数组首元素地址(arr == &arr[0])是一样的，
//                                 //传给strlen都是从起始位置就是数组第一个元素的位置开始计算，结果仍是随机的
//    printf("%d\n", strlen(&arr + 1));//&arr + 1是跳过整个数组后的位置的地址，从这个位置开始计算，也是随机值，但和上面的值不同
//    printf("%d\n", strlen(&arr[0] + 1));//&arr[0] + 1 == &arr[1],就是第2个也元素的地址，
//                                        //相当于是从第2个元素的位置往后计算，得到的也是随机值，值为strlen(arr) - 1
//    return 0;
//}

//代码3：
//int main()
//{
//    char arr[] = "abcdef"; //'a', 'b', 'c', 'd', 'e', 'f', '\0'
//    printf("%d\n", sizeof(arr));//sizeof(数组名)，数组名表示整个数组，计算的是整个数组的大小 - 7
//    printf("%d\n", sizeof(arr + 0));//和前面一样，括号里面不是单独数组名，所以a + 0还是首元素地址，是地址就是4/8
//    printf("%d\n", sizeof(*arr));//arr数组名就是数组首元素地址，对其解引用得到的就是首元素'a',类型是char，大小为1
//    printf("%d\n", sizeof(arr[1]));//arr[1]就是数组第2个元素'b'，类型是char，大小为1
//    printf("%d\n", sizeof(&arr));//&arr是数组指针，类型是char(*)[7],归根结底还是地址，是地址就是4/8
//    printf("%d\n", sizeof(&arr + 1));//&arr + 1是跳过整个数组后的位置的地址，是地址就是4/8
//    printf("%d\n", sizeof(&arr[0] + 1));//&arr[0] + 1 == &arr[1],就是第2个元素的地址，是地址就是4/8
//    return 0;
//}

//代码4：
//int main()
//{
//    char arr[] = "abcdef"; //'a', 'b', 'c', 'd', 'e', 'f', '\0'
//    printf("%d\n", strlen(arr));//不是两个例外，arr数组名就是数组首元素的地址，字符串后面默认有\0，则结果为6
//    printf("%d\n", strlen(arr + 0));//arr + 0还是数组首元素的地址，结果仍为6
//  //printf("%d\n", strlen(*arr));//arr是数组首元素的地址，对其解引用得到的是首元素'a'，并不是指针，和前面一样代码有问题
//  //printf("%d\n", strlen(arr[1]));//arr[1]就是数组第2个元素'b'，也不是指针，代码也有问题
//    printf("%d\n", strlen(&arr));//&arr是整个数组的地址，类型为 char(*)[7] ,但值和数组首元素地址(arr == &arr[0])是一样的，
//                                 //传给strlen都是从起始位置就是数组第一个元素的位置开始计算，结果为6
//    printf("%d\n", strlen(&arr + 1));//&arr+1是跳过整个数组后的位置的地址，
//                                     //后面的内容完全不知道并且\0何时出现也不知道，为随机值
//    printf("%d\n", strlen(&arr[0] + 1));//&arr[0] + 1 == &arr[1]就是数组第2个元素的地址，从第2个元素的位置开始计算，结果为5
//    return 0;
//}

//代码5：
//int main()
//{
//    const char* p = "abcdef";
//  //字符串本身并不是一个独立的数据类型，本质可以理解为以空字符'\0'结尾的字符数组，类型为char[N],N是包括'\0'在内的数组元素个数
//  //所以字符指针p接收的是字符串的地址，也能像数组名一样理解接收的就是首字符'a'的地址，即p就能近似理解为就是数组名
//    printf("%d\n", sizeof(p));//p为字符指针变量，类型是char*，指针就是地址，是地址大小就是4/8
//    printf("%d\n", sizeof(p + 1));//p为字符指针变量，指针加1跳过一个元素，p + 1就是字符'b'的地址，是地址就是4/8
//    printf("%d\n", sizeof(*p));//p的类型是char*，对p解引用得到的值的类型是char，则结果为1
//    printf("%d\n", sizeof(p[0]));//p[0]有两种理解方式：
//    //1.p[0] --> *(p + 0) --> *p --> 'a',大小为1个字节
//    //2.字符串本身并不是一个独立的数据类型，本质可以就理解为以空字符'\0'结尾的字符数组，则p就可以理解为数组名，p[0]就是首元素
//    printf("%d\n", sizeof(&p));//取出来的是p的地址，p本身就是字符指针，类型是char*，
//                               //则如果用变量q接收p的地址，q的类型就是char**，q就是二级指针，是指针sizeof就是4/8
//    printf("%d\n", sizeof(&p + 1));//由于上面假设了变量q接收p的地址，q的类型是char**，q + 1就是指针加1的形式
//                                   //表示的就是跳过该指针指向对象类型的大小的字节数，也就是跳过一个char*类型的大小(4个字节)
//                                   //最终表示的还是地址，是地址就是4/8
//    printf("%d\n", sizeof(&p[0] + 1));//上面已经把p[0]理解为首元素，对其取地址再加1 --> &p[1],
//                                      //就是字符串第2个字符'b'的地址，是地址就是4/8
//    return 0;
//}

//代码6：
//int main()
//{
//    char* p = "abcdef";
//    printf("%d\n", strlen(p));//p为字符指针变量，存放的是字符串的地址也就是字符'a'的地址，从'a'的位置往后计算，值为6
//    printf("%d\n", strlen(p + 1));//p为指针，类型为char*，p+1跳过了1个字节，就是第2个字符'b'的地址，往后计算，值为5
//  //printf("%d\n", strlen(*p));//p存放的就是字符'a'的地址，对其解引用得到的就是字符'a',而strlen需要传入地址，故代码有问题
//  //printf("%d\n", strlen(p[0]));//和上面一样理解成数组，p[0]指的就是首元素'a'，而strlen需要传入地址，故代码有问题
//    printf("%d\n", strlen(&p));//取出来的是p的地址，p本身就是字符指针，类型是char*，
//                               //则如果用变量q接收p的地址，q的类型就是char**，q就是二级指针，
//                               //q存放的是指针变量p的地址，虽然我们知道p存放的是字符串的地址，
//                               //但变量p自身的所在的地址我们完全不知道，故后面什么时候出现\0我们也不知道，结果为随机值
//    printf("%d\n", strlen(&p + 1));//也是随机值
//    printf("%d\n", strlen(&p[0] + 1));//p[0]就可以理解成第一个字符'a'，对其取地址再加1，就是第2个字符'b'的地址，&p[1]
//                                      //从第2个字符'b'的位置往后计算，值为5
//    return 0;
//}

//二维数组
//int main()
//{
//    int a[3][4] = { 0 };
//    printf("%d\n", sizeof(a));//a为数组名，并且单独放在sizeof内部，表示的是整个数组，a的类型为int[3][4],值为3*4*4=48
//    printf("%d\n", sizeof(a[0][0]));//a[0][0]为第一行第一列的元素，类型为int，大小为4个字节
//    printf("%d\n", sizeof(a[0]));//在之前我们就知道二维数组相当于就是把一维数组做为数组的元素
//                                 //所以a[0]其实就是第一行一维数组，以此类推a[1]就是第二行一维数组，a[2]就是第三行一维数组
//                                 //即a[0]为数组名，并且单独放在sizeof内部，表示的是整个一维数组，a[0]的类型为int[4],值为16
//    printf("%d\n", sizeof(a[0] + 1));//和前面的一样，sizeof内部并不是单独放数组名而是数组名加整数，
//                                     //所以此时a[0]表示的是第一行一维数组首元素的地址，
//                                     //a[0]+1就是第一行一维数组第2个元素地址，类型为int*，是地址 sizeof就是4/8
//    printf("%d\n", sizeof(*(a[0] + 1)));//a[0]+1就是第一行一维数组第2个元素地址，对其解引用得到的就是第一行第2个元素，值为4
//    printf("%d\n", sizeof(a + 1));//a为数组名且没有单独放在sizeof内部，表示的是二维数组首元素的地址，即第一行一维数组的地址
//                                  //类型为int[4],a+1跳过的是1*4*4=16个字节，得到的就是第二行一维数组的地址，是地址就是4/8
//    printf("%d\n", sizeof(*(a + 1)));//a+1是第二行一维数组的地址，类型为int*[4](数组指针)对其解引用得到的是整个第二行一维数组
//                                     //相当于一维数组单独放在sizeof内部，*(a+1)表示的就是整个一维数组，类型为int[4],值为16
//    printf("%d\n", sizeof(&a[0] + 1));//a[0]表示的就是第一行一维数组，对其取地址就是第一行一维数组的地址，类型是int(*)[4]
//                                      //再加1跳过的是16个字节，得到的是第二行一维数组的地址，是地址sizeof就是4/8
//    printf("%d\n", sizeof(*(&a[0] + 1)));//由于不是两个例外，此时&a[0] == a就是第一行一维数组的地址，
//                                         //加1得到的就是第二行数组的地址，对其解引用得到的就是第二行数组，
//                                         //数组名单独放在sizeof内部，表示的就是整个一维数组，值为16
//    printf("%d\n", sizeof(*a));//由于a并不是单独放在sizeof内部，数组名表示的是二维数组首元素地址即第一行一维数组的地址
//                               //对其解引用得到的就是第一行一维数组，一维数组单独放在sizeof内部则表示整个一维数组，值为16
//    printf("%d\n", sizeof(a[3]));//虽然a[3]正常来说是越界访问了，但sizeof只需要判断类型就能计算大小
//                                 //也就是a[3]无需真实存在，a[3]是第四行的数组名，类型为int[4],
//                                 //并且单独放在sizeof内部，表示的就是整个数组，值仍为16
//                                 //本质就是sizeof是不会真实访问空间，就不会存在越界访问这一说，只需知道数据类型即可计算大小
//    return 0;
//}

//指针运算笔试题解析
//题目1：
//int main()
//{
//    int a[5] = { 1, 2, 3, 4, 5 };
//    int* ptr = (int*)(&a + 1);
//    printf("%d,%d", *(a + 1), *(ptr - 1));//2 ,5
//    //a为数组名，表示的是数组首元素地址，加1得到的是数组第2个元素的地址，对其解引用*(a + 1)得到的就是第2个元素，就是2
//    //首先&a，a为数组名，表示的是整个数组，&a类型为int(*)[5],加1跳过的是4*5=20个字节，即跳过了整个数组
//    //指针加1类型不变，&a + 1类型仍为int(*)[5],由于变量ptr类型为int*，所以在赋值之前需要强制类型转换成int*
//    //此时ptr类型为int*，减1回退的是1*4/8个字节，得到的就是数组最后一个元素的地址，对其解引用就是数组最后一个元素5
//    return 0;
//}

//题目2：
//在X86环境下 
//假设结构体的大小是20个字节  
//struct Test
//{
//    int Num;
//    char* pcName;
//    short sDate;
//    char cha[2];
//    short sBa[4];
//}*p = (struct Test*)0x100000; //0x是十六进制的标志，0x1就是十六进制的1
//
//int main()
//{
//    printf("%p\n", p + 0x1); //00100014
//    printf("%p\n", (unsigned long)p + 0x1); //00100001
//    printf("%p\n", (unsigned int*)p + 0x1); //00100004
//    //p是结构体指针变量，类型为struct Test*，将十六进制0x100000为整型，强制类型转换成结构体指针类型赋给p
//    //p + 0x1就是p + 1，为指针加整数的形式，表示的是跳过指向对象的类型的大小的字节数
//    //由于结构体大小为20个字节，则p + 1跳过20个字节，20转换为十六进制为14，则打印地址为00100014
//
//    //(unsigned long)p是把类型struct Test*的变量p强制类型转换成了无符号长整型类型，此时p就不在是指针了
//    //也就不适用指针加整数的形式，此时加1就是整型数据加1，打印地址为00100001
//
//    //(unsigned int*)p是把类型struct Test*的变量p强制类型转换成了无符号整型指针类型，
//    //还是指针加整数的形式，由于p指向的对象的类型变为无符号整型类型，则p + 1跳过4个字节，打印地址为00100004
//    return 0;
//}

//题目3：
//int main()
//{
//    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
//    int* p;
//    p = a[0];//a[0]表示的就是第一行一维数组
//    printf("%d", p[0]); //p[0]表达的就是第一行一维数组下标为0的元素，就是1
//    return 0;
//    //这个代码最重要的就是数组a里面的内容，很多人下意识会误以为0 1 2 3 4 5按照三行两列顺着排，但这种里面也必须是大括号才符合
//    //这个数组里面的元素都是又小括号构成的，里面还有逗号，这种形式是逗号表达式
//    //逗号表达式从左向右依次计算，最后一个表达式的结果为整个表达式的结果
//    //所以a[3][2] = { (0, 1), (2, 3), (4, 5) } == { 1, 3, 5 }
//}

//题目4：
//假设环境是x86环境，程序输出的结果是啥？ 
//int main()
//{
//    int a[5][5];
//    int(*p)[4];//p是一个数组指针类型，类型为int(*)[4]
//    p = a;//由于a的类型为int(*)[5],与p定义的类型不完全一样，在赋值的时候类型不匹配会报警告，但能运行代码
//    printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
//    //在之前我们知道二维数组就是以多个一维数组为元素进行排列，并且这些一维数组是连续存放的
//    //所以可以把a的5个一维数组在一行挨着进行排列，假设为总共25个格子，一个格子代表一个元素的地址
//    //&a[4][2] == (*(a + 4) + 2),a + 4指的是跳过4*20=80个字节到第4个一维数组的地址，
//    //对其解引用得到的就是第4个数组，数组名就是首元素的地址，
//    //再加2指的就是跳过2*4=8个字节得到的就是第4个数组的第3个元素的地址(即第23个格子)
//
//    //&p[4][2]和&a[4][2]是有很大差异的，差异就是来自他们的类型不同，
//    //&p[4][2] == (*(p + 4) + 2)，这个形式和a看起来一样，但是在定义变量p时，p的类型是int(*)[4],
//    //所以在p + 4的时候跳过的字节数是p指向对象的类型(int[4])的大小，即跳过4*4=16个字节，这就和a + 4是不一样了
//    //但是p = a是把a首元素的地址赋给p，相当于p和a的起始位置是一样的，
//    //则*(p + 4)来到的位置对于a来说就是第4行一维数组的第2个元素的位置(即第17个格子)
//    //由于p类型为int(*)[4],指针加整数不会改变类型，所以解引用*(p + 4)后的类型为int[4],为数组类型
//    //是数组名就是首元素地址，再加2指的就是跳过2*4=8个字节，对于a来说得到的就是第4行一维数组的第4个元素的地址(即第19个格子)
//    //这里画图会更加清晰
//
//    //而指针减指针得到的是两个地址之间的元素个数，则23 - 19 == 4
//    //又因为&p[4][2]的地址比&a[4][2]小，则%d是以有符号的整数打印，得到的值为-4
//    //-4的补码为11111111111111111111111111111100
//    //以%d打印时是需要换算成原码形式再打印，则得到的值为-1
//    //而%p是打印地址，地址是没有补码原码一说，内存存的是补码，地址打印出来的就是补码
//    //而%p是以十六进制的形式打印，所以11111111111111111111111111111100要转换为十六进制形式：FFFFFFFFC
//    return 0;
//}

//题目5：
//int main()
//{
//    int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//    int* ptr1 = (int*)(&aa + 1);
//    int* ptr2 = (int*)(*(aa + 1));
//    printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
//    return 0;
//    //首先aa是二维数组，&数组名，数组名表示的就是整个二维数组，对其加1就是跳过整个二维数组的地址，
//    //然后将其强制类型转换成int*，再赋给ptr1变量，ptr1变量类型为整型指针类型，
//    //减1回退的字节数为ptr1变量指向对象的类型(int)的大小，故回退1*4=4个字节，即为&aa[1][4]
//    //对其解引用得到的就是元素10
//
//    //aa为二维数组，类型为int(*)[5],加1跳过1*20=20个字节，就是跳过第一行一维数组，为第二行一维数组的地址
//    //对其解引用得到的就是第二行一维数组，数组名就是首元素6的地址
//    //再将其强制类型转换成int*，再赋给ptr2，ptr2变量类型为整型指针类型，
//    //减1回退的字节数为ptr2变量指向对象的类型(int)的大小，故回退1*4=4个字节，即为&aa[0][4]
//    //对其解引用得到的就是元素5
//}

//题目6：
//int main()
//{
//    char* a[] = { "work","at","alibaba" };
//    char** pa = a;
//    pa++;
//    printf("%s\n", *pa);
//    return 0;
//    //a是指针数组，由于char* p = "abcdef"相当于是把首元素a的地址存放给p指针变量
//    //则"work","at","alibaba"相当于是分别把三个字符串的首元素的地址放在指针数组a里面
//    //char** pa = a这条代码有点绕，首先a是数组名，数组名就是数组首元素的地址，
//    //但是a第一个元素是第一个字符串首元素'a'的地址，这两者是有区别的，
//    //相当于是把第一个字符串首元素'a'的地址的地址赋给pa，
//    //所以pa为二级指针，指向的对象就是第一个字符串首元素'a'的地址，pa的类型为char**
//    //pa++ == pa + 1,即跳过1个pa指向的对象类型(char*)大小的字节数，
//    //又因为数组a有三个类型为char*的元素，跳过一个类型为char*大小的字节数，就到第二个元素的地址
//    //对其解引用得到的就是第二个元素，而第二个元素就是第二个字符串首元素'a'的地址
//    //%s打印字符串，给一个地址，从这个地址向后打印字符串，直到\0截至，则打印的结果为at
//}

//题目7：(代码逻辑是很复杂麻烦的，涉及到三级指针，最好是边画图边找地址之间的联系)
//int main()
//{
//    char* c[] = { "ENTER","NEW","POINT","FIRST" };
//    char** cp[] = { c + 3,c + 2,c + 1,c };
//    char*** cpp = cp;
//    //首先和上面一题一样，c为字符指针数组，存放的元素分别是四个字符串首字符的地址
//    //然后定义cp变量为二级指针数组，由于c为数组名就是数组首元素的地址(即第一个字符串首字符的地址的地址)，类型就是char**
//    //而指针加整数不会改变类型，所以cp数组存放的元素类型全为char**
//    //又因为cp也是数组名，数组名就是数组首元素地址(即元素c + 3的地址)，这里已经是三级指针了，类型为char***，再赋给cpp
//
//    printf("%s\n", **++cpp);//POINT
//    //首先++cpp，也就是cpp + 1，但是++运算符还会保留cpp得到的新的值，这里是非常重要的，后面代码的cpp都是基于新值进行运算
//    //指针加1跳过的字节数为指向的对象类型(char**)大小的字节数，即跳到(c + 2)元素的地址，
//    //对其解引用得到的就是(c + 2)这个元素，而(c + 2)就是跳过两个类型(char*)大小的字节数，
//    //就是c数组的第三个元素的地址，即第三个字符串首元素的地址的地址
//    //再对其解引用得到的就是第三个字符串首元素的地址，%s打印的就是POINT
//
//    printf("%s\n", *-- * ++cpp + 3);//ER
//    //这里的cpp就是基于上面的新值即(c + 2)元素的地址，首先++cpp，一样的操作
//    //得到的就是(c + 1)元素的地址，对其解引用得到的就是(c + 1)元素
//    //然后再-- (c + 1)，也就是(c + 1) - 1 == c，即数组c首元素的地址
//    //再对其解引用得到的就是首元素，即第一个字符串首字符的地址
//    //再加3，跳过的是指向对象类型(char)大小的字节数，即为字符'E'的地址，%s打印的就是ER
//
//    printf("%s\n", *cpp[-2] + 3);//ST
//    //这里的cpp就是基于上面代码cpp的新值即(c + 1)元素的地址，
//    //之前我们学习了数组名[1]就等价于*(数组名 + 1)，所以类比到这里
//    //cpp[-2] == *(cpp - 2),而cpp是(c + 1)元素的地址，减2即回退到(c + 3)元素的地址，对其解引用得到的就是(c + 3)元素
//    //然后再对其解引用得到的就是第四个字符串首字符的地址，加3即为字符'S'的地址，%s打印的就是ST
//
//    printf("%s\n", cpp[-1][-1] + 1);//EW
//    //这里只要中的数组名[1][1]等价于就是*(*(数组名 + 1) + 1)，则和上面的代码本质一样
//    // 类比到这里cpp[-1][-1] == *(*(cpp - 1) - 1)
//    //首先cpp还是(c + 1)元素的地址，减1则回退到(c + 2)元素的地址
//    //对其解引用得到的就是(c + 2)元素，再减1，回退到的就是数组c第二个元素的地址，即第二个字符串首字符地址的地址
//    //再对其解引用得到的就是第二个字符串首字符的地址，再加1即为字符'E'的地址，%s打印的就是EW
//
//    return 0;
//}